/*!
 * ***** BEGIN LICENSE BLOCK *****
 *   Version: GPL 2.0
 *
 * JQGTK+ Color Selector: A jQuery-compatible JavaScript color selector based on
 * the GTK+ Color Selector which is part of the GIMP Toolkit (GTK+).
 *
 * Copyright (C) 2010 Adam Dane
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * ***** END LICENSE BLOCK ***** */
/** @license
 * ***** BEGIN LICENSE BLOCK *****
 *   Version: GPL 2.0
 *
 * JQGTK+ Color Selector: A jQuery-compatible JavaScript color selector based on
 * the GTK+ Color Selector which is part of the GIMP Toolkit (GTK+).
 *
 * Copyright (C) 2010 Adam Dane
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * ***** END LICENSE BLOCK ***** */
(function ($)
  {
/* BEGIN PRIVATE FUNCTIONS */
    function linInterp(a, b, v1Diff, vDiff)
    {
      return a + (b - a) * v1Diff / vDiff;
    }

    function clamp(x, low, high)
    {
      return x > high ? high : x < low ? low : x;
    }

    function max3(a, b, c)
    {
      if (a > b)
      {
        if (a > c)
        {
          return a;
        }
        else
        {
          return c;
        }
      }
      else
      {
        if (b > c)
        {
          return b;
        }
        else
        {
          return c;
        }
      }
    }

    function min3(a, b, c)
    {
      if (a > b)
      {
        if (b > c)
        {
          return c;
        }
        else
        {
          return b;
        }
      }
      else
      {
        if (a > c)
        {
          return c;
        }
        else
        {
          return a;
        }
      }
    }

    function getColorBrightness(r, g, b)
    {
      var rBright = r * 299;
      var gBright = g * 587;
      var bBright = b * 114;
      return (rBright + gBright + bBright) / 1000;
    }

    function getValueDiff(v1, v2)
    {
      return Math.abs(v1 - v2);
    }

    function getBrightnessDiff(color1, color2)
    {
      var c1Bright = color1.getColorBrightness();
      var c2Bright;
      if (color2.getColorBrightness)
      {
        c2Bright = color2.getColorBrightness();
      }
      else
      {
        c2Bright = getColorBrightness(color2.red, color2.green, color2.blue);
      }
      return getValueDiff(c1Bright, c2Bright);
    }

    function getValueRatio(v1, v2)
    {
      var vMax = Math.max(v1, v2);
      var vMin = Math.min(v1, v2);
      return vMax / vMin;
    }

    function getColorDiff(color1, color2)
    {
      var rDiff = getValueDiff(color1.red, color2.red);
      var gDiff = getValueDiff(color1.green, color2.green);
      var bDiff = getValueDiff(color1.blue, color2.blue);
      return rDiff + gDiff + bDiff;
    }

    function getLuminanceColor(channel)
    {
      channel /= 255;
      if (channel <= 0.03928)
      {
        return channel / 12.92;
      }
      else
      {
        return Math.pow(((channel + 0.055) / 1.055), 2.4);
      }
    }

    function getLuminance(r, g, b)
    {
      var rLum = getLuminanceColor(r) * 0.2126;
      var gLum = getLuminanceColor(g) * 0.7152;
      var bLum = getLuminanceColor(b) * 0.0722;
      return rLum + gLum + bLum;
    }

    function getLumRatio(lum1, lum2)
    {
      return getValueRatio(lum1 + 0.05, lum2 + 0.05);
    }

    function getColorLumRatio(color1, color2)
    {
      var lum1 = color1.getLuminance();
      var lum2;
      if (color2.getLuminance)
      {
        lum2 = color2.getLuminance();
      }
      else
      {
        lum2 = getLuminance(color2.red, color2.green, color2.blue);
      }
      return getLumRatio(lum1, lum2);
    }

    function colorString(r, g, b)
    {
      var red = Math.floor(r + 0.5);
      var green = Math.floor(g + 0.5);
      var blue = Math.floor(b + 0.5);
      var rString = (red < 16) ? "0" + red.toString(16) : red.toString(16);
      var gString = (green < 16) ?
        "0" + green.toString(16) : green.toString(16);
      var bString = (blue < 16) ? "0" + blue.toString(16) : blue.toString(16);
      return ("#" + rString + gString + bString).toUpperCase();
    }

    function rgbToHSV(color)
    {
      var h, s, v;
      var r = color.red;
      var g = color.green;
      var b = color.blue;
      var rgbMax = max3(r, g, b);
      var rgbMin = min3(r, g, b);

      v = rgbMax;
      if (rgbMax === 0)
      {
        h = s = 0;
        return { hue: h, saturation: s, value: v };
      }

      r /= v;
      g /= v;
      b /= v;
      rgbMin = min3(r, g, b);
      rgbMax = max3(r, g, b);
      s = rgbMax - rgbMin;
      if (s === 0)
      {
        h = 0;
        return { hue: h, saturation: s, value: (v / 255) * 100 };
      }
      r = (r - rgbMin) / s;
      g = (g - rgbMin) / s;
      b = (b - rgbMin) / s;
      rgbMin = min3(r, g, b);
      rgbMax = max3(r, g, b);
      if (rgbMax === r)
      {
        h = 0.0 + 60.0 * (g - b);
        if (h < 0)
        {
          h += 360.0;
        }
      }
      else if (rgbMax === g)
      {
        h = 120.0 + 60.0 * (b - r);
      }
      else
      {
        h = 240.0 + 60.0 * (r - g);
      }
      return {
        hue: h,
        saturation: s * 100,
        value: (v / 255) * 100
      };
    }

    function hsvToRGB(hsvColor) // hue, saturation, value
    {
      var h = hsvColor.hue;
      var s = hsvColor.saturation;
      var v = hsvColor.value;
      h /= 360;
      s /= 100;
      v /= 100;

      if (s === 0)
      {
        v = v * 255;
        return { red: v, green: v, blue: v };
      }

      h *= 6;
      if (h === 6)
      {
        h = 0;
      }

      var hueCase = Math.floor(h);
      var f = h - hueCase;
      var p = 255 * v * (1 - s);
      var q = 255 * v * (1 - s * f);
      var t = 255 * v * (1 - s * (1 - f));

      v = v * 255;

      switch (hueCase)
      {
      case 0:
        return { red: v, green: t, blue: p };
      case 1:
        return { red: q, green: v, blue: p };
      case 2:
        return { red: p, green: v, blue: t };
      case 3:
        return { red: p, green: q, blue: v };
      case 4:
        return { red: t, green: p, blue: v };
      default:
        return { red: v, green: p, blue: q };
      }
    }

    function hexColor(aColStr)
    {
      var strLen = aColStr.length;
      var r, g, b;
      if (strLen === 7)
      {
        if (aColStr.charAt(0) !== "#")
        {
          throw "Input invalid.";
        }
        else
        {
          r = aColStr.substr(1, 2);
          g = aColStr.substr(3, 2);
          b = aColStr.substr(5, 2);
        }
      }
      else if (strLen === 6)
      {
        r = aColStr.substr(0, 2);
        g = aColStr.substr(2, 2);
        b = aColStr.substr(4, 2);
      }
      else if (strLen === 4)
      {
        if (aColStr.charAt(0) !== "#")
        {
          throw "Input invalid.";
        }
        else
        {
          r = aColStr.substr(1, 1);
          g = aColStr.substr(2, 1);
          b = aColStr.substr(3, 1);
          r += r;
          g += g;
          b += b;
        }
      }
      else if (strLen === 3)
      {
        r = aColStr.substr(0, 1);
        g = aColStr.substr(1, 1);
        b = aColStr.substr(2, 1);
        r += r;
        g += g;
        b += b;
      }
      else
      {
        // bad input for sure.
        throw "Input length invalid.";
      }
      return {
        red: parseInt(r, 16),
        green: parseInt(g, 16),
        blue: parseInt(b, 16)
      };
    }

    function Color(data)
    {
      var tmp;
      if (typeof(data) === "string")
      {
        tmp = hexColor(data);
        this.red = tmp.red;
        this.green = tmp.green;
        this.blue = tmp.blue;
        tmp = rgbToHSV(tmp);
        this.hue = tmp.hue;
        this.saturation = tmp.saturation;
        this.value = tmp.value;
      }
      else if (typeof(data.red) !== "undefined")
      {
        this.red = parseFloat(data.red, 10);
        this.green = parseFloat(data.green, 10);
        this.blue = parseFloat(data.blue, 10);
        tmp = rgbToHSV({ red: this.red, green: this.green, blue: this.blue });
        this.hue = tmp.hue;
        this.saturation = tmp.saturation;
        this.value = tmp.value;
      }
      else if (typeof(data.hue) !== "undefined")
      {
        this.hue = parseFloat(data.hue);
        this.saturation = parseFloat(data.saturation);
        this.value = parseFloat(data.value);
        tmp = hsvToRGB(
          {
            hue: this.hue,
            saturation: this.saturation,
            value: this.value
          });
        this.red = tmp.red;
        this.green = tmp.green;
        this.blue = tmp.blue;
      }
      else
      {
        throw "Color: The data is not recognized.";
      }
    }

    Color.prototype.rgbString = function ()
    {
      var rgbArr =
        [
          Math.floor(this.red + 0.5),
          Math.floor(this.green + 0.5),
          Math.floor(this.blue + 0.5)
        ];
      return ['rgb(', rgbArr.join(', '), ')'].join('');
    };

    Color.prototype.colorString = function ()
    {
      var red = Math.floor(this.red + 0.5);
      var green = Math.floor(this.green + 0.5);
      var blue = Math.floor(this.blue + 0.5);
      var rString = (red < 16) ?
        "0" + red.toString(16) : red.toString(16);
      var gString = (green < 16) ?
        "0" + green.toString(16) : green.toString(16);
      var bString = (blue < 16) ?
        "0" + blue.toString(16) : blue.toString(16);
      return ("#" + rString + gString + bString).toUpperCase();
    };

    Color.prototype.intensity = function ()
    {
      var rInt = (this.red / 255) * 0.30;
      var gInt = (this.green / 255) * 0.59;
      var bInt = (this.blue / 255) * 0.11;
      return rInt + gInt + bInt;
    };

    Color.prototype.hueIncrement = function ()
    {
      var h = this.hue;
      if (h === 360)
      {
        h = 0;
      }
      else
      {
        h += 1;
      }
      this.setHue(h);
    };

    Color.prototype.hueDecrement = function ()
    {
      var h = this.hue;
      if (h === 0)
      {
        h = 360;
      }
      else
      {
        h -= 1;
      }
      this.setHue(h);
    };

    Color.prototype.setHue = function (newVal)
    {
      this.hue = clamp(newVal, 0, 360);
      var tmp = hsvToRGB(
        {
          hue: this.hue,
          saturation: this.saturation,
          value: this.value
        });
      this.red = tmp.red;
      this.green = tmp.green;
      this.blue = tmp.blue;
    };

    Color.prototype.setSaturation = function (newVal)
    {
      this.saturation = clamp(newVal, 0, 100);
      var tmp = hsvToRGB(
        {
          hue: this.hue,
          saturation: this.saturation,
          value: this.value
        });
      this.red = tmp.red;
      this.green = tmp.green;
      this.blue = tmp.blue;
    };

    Color.prototype.desaturated = function ()
    {
      return new Color(
        {
          hue: this.hue,
          saturation: clamp(this.saturation - 50, 0, 50),
          value: this.value
        }).rgbString();
    };

    Color.prototype.setValue = function (newVal)
    {
      this.value = clamp(newVal, 0, 100);
      var tmp = hsvToRGB(
        {
          hue: this.hue,
          saturation: this.saturation,
          value: this.value
        });
      this.red = tmp.red;
      this.green = tmp.green;
      this.blue = tmp.blue;
    };

    Color.prototype.setRed = function (newVal)
    {
      this.red = clamp(newVal, 0, 255);
      var tmp = rgbToHSV({ red: this.red, green: this.green, blue: this.blue });
      this.hue = tmp.hue;
      this.saturation = tmp.saturation;
      this.value = tmp.value;
    };

    Color.prototype.setGreen = function (newVal)
    {
      this.green = clamp(newVal, 0, 255);
      var tmp = rgbToHSV({ red: this.red, green: this.green, blue: this.blue });
      this.hue = tmp.hue;
      this.saturation = tmp.saturation;
      this.value = tmp.value;
    };

    Color.prototype.setBlue = function (newVal)
    {
      this.blue = clamp(newVal, 0, 255);
      var tmp = rgbToHSV({ red: this.red, green: this.green, blue: this.blue });
      this.hue = tmp.hue;
      this.saturation = tmp.saturation;
      this.value = tmp.value;
    };

    Color.prototype.setHex = function (newVal)
    {
      var tmp;
      tmp = hexColor(newVal);
      this.red = tmp.red;
      this.green = tmp.green;
      this.blue = tmp.blue;
      tmp = rgbToHSV(tmp);
      this.hue = tmp.hue;
      this.saturation = tmp.saturation;
      this.value = tmp.value;
    };

    Color.prototype.getColorBrightness = function ()
    {
      var rBright = this.red * 299;
      var gBright = this.green * 587;
      var bBright = this.blue * 114;
      return (rBright + gBright + bBright) / 1000;
    };

    Color.prototype.getLuminance = function ()
    {
      var rLum = getLuminanceColor(this.red) * 0.2126;
      var gLum = getLuminanceColor(this.green) * 0.7152;
      var bLum = getLuminanceColor(this.blue) * 0.0722;
      return rLum + gLum + bLum;
    };

    Color.prototype.isCompatible = function (col2)
    {
      var lDiff = getColorLumRatio(this, col2);
      var lPass = lDiff >= 7;
      var bDiff = getBrightnessDiff(this, col2);
      var bPass = bDiff >= 125;
      var cDiff = getColorDiff(this, col2);
      var cPass = cDiff >= 500;
      return lPass && bPass && cPass;
    };

    function getUnusedId(partial)
    {
      var newID;
      var idSet = [];
      try
      {
        $("[id ^= '" + partial + "']").each(function (index, element)
          {
            idSet.push($(element).attr('id'));
          });
        idSet.sort();
        newID = parseInt(idSet[0].split('_').pop(), 10) + 1;
      }
      catch (ex)
      {
        newID = 0;
      }
      return partial + newID;
    }

    function Field(opts)
    {
      this.fieldDiv = $(
        "<div />",
        {
          "class": [''].concat(
            opts["class"].split(' ')).join(' colorselect-').slice(1)
        })[0];
      this.fieldID = getUnusedId(opts.id);
      this.fieldLabel = $(
        "<label />",
        {
          "for": this.fieldID
        }).text(opts.label).appendTo(this.fieldDiv)[0];
      this.field = $(
        "<input />",
        {
          id: this.fieldID,
          type: opts.type
        }).appendTo(this.fieldDiv)[0];
      this.oldValue = null;
      this.type = opts.type;
      if (this.type === "number")
      {
        $(this.field).attr("role", "spinbutton");
        if (typeof(opts.valuemin) !== "undefined")
        {
          this.minValue = opts.valuemin;
          $(this.field).attr(
            {
              "min": opts.valuemin,
              "aria-valuemin": opts.valuemin
            });
          if (typeof(opts.valuemax) !== "undefined")
          {
            this.maxValue = opts.valuemax;
            $(this.field).attr(
              {
                "max": opts.valuemax,
                "aria-valuemax": opts.valuemax
              });
          }
        }
      }
      else if (this.type === "checkbox")
      {
        $(this.field).attr(
          {
            "value": opts.value,
            "checked": opts.checkstate || false,
            "aria-checked": opts.checkstate || false,
            "role": "checkbox"
          });
      }
      else if (this.type === "text")
      {
        $(this.field).attr(
          {
            "role": "textbox"
          });
      }
      if (typeof(opts.eventHandler) === "function")
      {
        if (this.type === "checkbox")
        {
          $(this.field).click(opts.eventHandler);
        }
        else
        {
          $(this.field).blur(opts.eventHandler);
          $(this.field).keypress(opts.eventHandler);
        }
      }
    }

    Field.prototype.setInternalValue = function (newVal)
    {
      // Returns true if the internal value changes, false otherwise;
      // this is used to keep the focus handlers from changing values every
      // time if no actual change was made.
      if (this.type === "number")
      {
        newVal = Math.floor(newVal);
      }
      if (this.oldValue !== null && newVal === this.oldValue)
      {
        return { value: this.oldValue, changed: false };
      }
      this.oldValue = newVal;
      return { value: newVal, changed: true };
    };

    Field.prototype.setValue = function (newVal)
    {
      this.field.value = this.setInternalValue(newVal).value;
    };

    function Fieldset(aClass, aDesc)
    {
      this.fieldset = $(
        "<fieldset></fieldset>",
        {
          "class": [''].concat(aClass.split(
            ' ')).join(' colorselect-').slice(1)
        })[0];
      if (aDesc)
      {
        this.description = $(
          "<legend></legend>").text(aDesc).appendTo(this.fieldset)[0];
      }
    }

    function ColorSelector(aContainer, opts)
    {
      this.ready = false;
      this.Errors =
      {
        notReady: "The ColorSelector has not been set up.",
        whereTo: "The ColorSelector requires a Container."
      };
      if (typeof(aContainer) !== "undefined")
      {
        this.container = aContainer;
        this.originalHTML = $(this.container).html();
        $(this.container).html("");
      }
      else
      {
        throw this.Errors.whereTo;
      }
      if (typeof(opts.color) !== "undefined")
      {
        this.color = new Color(opts.color);
      }
      else
      {
        this.color = new Color("#F00");
      }

      this.allowSwap = opts.allowSwap;
      if (typeof(opts.altColor) !== "undefined")
      {
        this.altColor = new Color(opts.altColor);
        this.useAltColor = true;
      }
      else
      {
        this.allowSwap = false;
        this.useAltColor = false;
      }
      this.compliance = opts.compliance;
      this.useRGBFields = opts.useRGBFields;
      this.useHSVFields = opts.useHSVFields;
      this.useColorField = opts.useColorField;
      this.sampleText = opts.sampleText;
      this.sampleTextSize = opts.sampleTextSize;
      this.sampleTextFont = opts.sampleTextFont;
      this.sampleTextFamily = opts.sampleTextFamily;
    }

    ColorSelector.prototype.remove = function ()
    {
      $(this.container).html(this.originalHTML);
    };

    ColorSelector.prototype.refresh = function ()
    {
      this.drawSelector();
    };

    ColorSelector.prototype.setColor = function (color)
    {
      this.color = new Color(color);
      $(this.container).trigger(
        'colorchange',
        [
          this.color.colorString()
        ]);
    };

    ColorSelector.prototype.setAltColor = function (color)
    {
      if (typeof(color) === "undefined")
      {
        if (this.altColor === "undefined")
        {
          this.altColor = new Color("#FFF");
        }
      }
      else
      {
        this.altColor = new Color(color);
      }
      $(this.container).trigger(
        'altcolorchange',
        [
          this.altColor.colorString()
        ]);
    };

    ColorSelector.prototype.setCompliance = function (value)
    {
      if (typeof(value) === "boolean")
      {
        this.compliance = value;
        if (!value)
        {
          $(this.compForms).hide();
        }
        else
        {
          // Implied
          this.setUseAltColor(true);
          $(this.compForms).show();
        }
      }
    };

    ColorSelector.prototype.setAllowSwap = function (value)
    {
      if (typeof(value) === "boolean")
      {
        this.allowSwap = value;
        if (!value)
        {
          $(this.swatchPara).hide();
        }
        else
        {
          // Implied
          this.setUseAltColor(true);
          $(this.swatchPara).show();
        }
      }
    };

    ColorSelector.prototype.setUseHSVFields = function (value)
    {
      if (typeof(value) === "boolean")
      {
        this.useHSVFields = value;
        if (!value)
        {
          $(this.hsvFields.fieldset).hide();
          if (!this.useRGBFields || !this.useColorField)
          {
            $(this.hRule).hide();
          }
        }
        else
        {
          $(this.hsvFields.fieldset).show();
          if (this.useColorField)
          {
            $(this.hRule).show();
          }
        }
      }
    };

    ColorSelector.prototype.setUseRGBFields = function (value)
    {
      if (typeof(value) === "boolean")
      {
        this.useRGBFields = value;
        if (!value)
        {
          $(this.rgbFields.fieldset).hide();
          if (!this.useHSVFields || !this.useColorField)
          {
            $(this.hRule).hide();
          }
        }
        else
        {
          $(this.rgbFields.fieldset).show();
          if (this.useColorField)
          {
            $(this.hRule).show();
          }
        }
      }
    };

    ColorSelector.prototype.setUseColorField = function (value)
    {
      if (typeof(value) === "boolean")
      {
        this.useColorField = value;
        if (!value)
        {
          $(this.cFields.fieldset).hide();
          $(this.hRule).hide();
        }
        else
        {
          $(this.cFields.fieldset).show();
          if (this.useHSVFields || this.useRGBFields)
          {
            $(this.hRule).show();
          }
        }
      }
    };

    ColorSelector.prototype.setUseColorSwatch = function (value)
    {
      if (typeof(value) === "boolean")
      {
        this.useColorSwatch = value;
        if (!value)
        {
          $(this.swatchWrap).hide();
        }
        else
        {
          $(this.swatchWrap).show();
        }
      }
    };

    ColorSelector.prototype.setUseAltColor = function (value)
    {
      if (typeof(value) === "boolean")
      {
        this.useAltColor = value;
        if (!value)
        {
          // Implied
          this.setAllowSwap(false);
          this.setCompliance(false);
          $(this.acField.fieldDiv).hide();
          $(this.altSwatch).hide();
          $(this.swatchLabels).hide();
        }
        else
        {
          // Implied
          this.setUseColorSwatch(true);
          this.setUseColorField(true);
          $(this.acField.fieldDiv).show();
          this.setAltColor();
          $(this.altSwatch).show();
          $(this.swatchLabels).show();
        }
      }
    };

    ColorSelector.prototype.setSampleText = function (value)
    {
      this.sampleText = value;
    };

    ColorSelector.prototype.setSampleTextSize = function (value)
    {
      this.sampleTextSize = value;
    };

    ColorSelector.prototype.setSampleTextFont = function (value)
    {
      this.sampleTextFont = value;
    };

    ColorSelector.prototype.setSampleTextFamily = function (value)
    {
      this.sampleTextFamily = value;
    };

    ColorSelector.prototype.initialize = function (aColor, altColor)
    {
      this.focus = this.oldFocus = null;
      this.focusHandler = this.createFocusHandler();
      this.blurHandler = this.createBlurHandler();
      this.triangleSelector = $(
        "<div />",
        {
          "class": "colorselect-selector",
          tabindex: 0,
          role: "presentation",
          focus: this.focusHandler,
          blur: this.blurHandler
        }).appendTo(this.container)[0];
      this.wrap = $(
        "<div />",
        {
          "class": "colorselect-canvaswrap",
          tabindex: 0,
          role: "presentation",
          focus: this.focusHandler,
          blur: this.blurHandler
        }).appendTo(this.container)[0];
      this.dragHandler = this.createDragHandler();
      this.cleanHandler = this.createCleanHandler();
      this.triangleHandler = this.createTriangleHandler();
      this.selectorHandler = this.createSelectorHandler();
      this.inMouseDown = false;
      this.canvas = $(
        "<canvas />",
        {
          "class": "colorselect-colorcanvas",
          id: getUnusedId("colorselect-canvas_"),
          role: "presentation",
          mousedown: this.dragHandler
        }).appendTo(this.wrap)[0];
      this.ringSelector = $(
        "<div />",
        {
          "class": "colorselect-selector",
          tabindex: 0,
          role: "presentation",
          focus: this.focusHandler,
          blur: this.blurHandler
        }).appendTo(this.container)[0];
      this.canvas.width = this.canvas.height = 170;
      this.context = this.canvas.getContext('2d');
      this.img = this.context.createImageData(170, 170);
      this.fakeCanvas = $("<canvas />")[0];
      this.fakeCanvas.width = this.fakeCanvas.height = 170;
      this.fakeCt = this.fakeCanvas.getContext('2d');

      this.forms = $(
        "<form></form>",
        {
          "class": "colorselect-colorselection"
        }).appendTo(this.container)[0];
      this.fieldHandler = this.createFieldHandler();
      this.hField = new Field(
        {
          label: "Hue:",
          type: "number",
          "class": "hsv canvascolor",
          eventHandler: this.fieldHandler,
          valuemin: 0,
          valuemax: 100,
          id: "colorselect-hsv-h_"
        });
      this.sField = new Field(
        {
          label: "Saturation:",
          type: "number",
          "class": "hsv canvascolor",
          eventHandler: this.fieldHandler,
          valuemin: 0,
          valuemax: 100,
          id: "colorselect-hsv-s_"
        });
      this.vField = new Field(
        {
          label: "Value:",
          type: "number",
          "class": "hsv canvascolor",
          eventHandler: this.fieldHandler,
          valuemin: 0,
          valuemax: 100,
          id: "colorselect-hsv-v_"
        });
      this.hsvFields = new Fieldset("hsvset");
      $(this.hsvFields.fieldset).append(
        [
          this.hField.fieldDiv,
          this.sField.fieldDiv,
          this.vField.fieldDiv
        ]);
      $(this.forms).append(this.hsvFields.fieldset);
      if (!this.useHSVFields)
      {
        $(this.hsvFields.fieldset).hide();
      }
      this.rField = new Field(
        {
          label: "Red:",
          type: "number",
          "class": "rgb canvascolor",
          eventHandler: this.fieldHandler,
          valuemin: 0,
          valuemax: 255,
          id: "colorselect-rgb-r_"
        });
      this.gField = new Field(
        {
          label: "Green:",
          type: "number",
          "class": "rgb canvascolor",
          eventHandler: this.fieldHandler,
          valuemin: 0,
          valuemax: 255,
          id: "colorselect-rgb-g_"
        });
      this.bField = new Field(
        {
          label: "Blue:",
          type: "number",
          "class": "rgb canvascolor",
          eventHandler: this.fieldHandler,
          valuemin: 0,
          valuemax: 255,
          id: "colorselect-rgb-b_"
        });
      this.rgbFields = new Fieldset("rgbset");
      $(this.rgbFields.fieldset).append(
        [
          this.rField.fieldDiv,
          this.gField.fieldDiv,
          this.bField.fieldDiv
        ]);
      $(this.forms).append(this.rgbFields.fieldset);
      if (!this.useRGBFields)
      {
        $(this.rgbFields.fieldset).hide();
      }
      this.hRule = $(
        "<hr />",
        {
          "class": "colorselect-ruler"
        }).appendTo(this.forms);
      this.cField = new Field(
        {
          label: "Color name:",
          type: "text",
          "class": "hex canvascolor",
          eventHandler: this.fieldHandler,
          id: "colorselect-hex-c_"
        });
      this.cFields = new Fieldset("hexset");
      $(this.cFields.fieldset).append(this.cField.fieldDiv);
      var canvasLabels = [];
      $(".colorselect-canvascolor input", this.forms).each(
        function (index, element)
        {
          canvasLabels.push($(element).attr("id"));
        });
      $(this.canvas).attr("aria-labelledby", canvasLabels.join(' '));
      this.acField = new Field(
        {
          label: "Alt name:",
          type: "text",
          "class": "hex",
          id: "colorselect-hex-a_"
        });
      $(this.acField.field).attr(
        {
          "aria-readonly": true,
          "readonly": "1"
        });
      $(this.cFields.fieldset).append(this.acField.fieldDiv);
      $(this.forms).append(this.cFields.fieldset);
      if (!(this.useHSVFields || this.useRGBFields) || !this.useColorField)
      {
        $(this.hRule).hide();
      }
      if (!this.useColorField)
      {
        $(this.cFields.fieldset).hide();
      }
      else if (!this.useAltColor)
      {
        $(this.acField.fieldDiv).hide();
      }
      this.swapHandler = this.createSwapHandler();
      this.swapKeyed = false;
      this.swatchWrap = $(
        "<div />",
        {
          "class": "colorselect-swatchwrap",
          role: "presentation"
        }).appendTo(this.container)[0];
      this.swatches = $(
        "<div />",
        {
          "class": "colorselect-colorswatches",
          tabindex: 0,
          role: "presentation"
        }).appendTo(this.swatchWrap)[0];
      this.swatch = $(
        "<canvas />",
        {
          "class": "colorselect-colorswatch",
          id: getUnusedId("colorselect-swatch_"),
          role: "presentation",
          click: this.swapHandler,
          keyup: this.swapHandler,
          keydown: this.swapHandler
        }).appendTo(this.swatches)[0];
      this.swatchCt = this.swatch.getContext('2d');
      this.altSwatch = $(
        "<canvas />",
        {
          "class": "colorselect-colorswatch",
          id: getUnusedId("colorselect-altswatch_"),
          role: "presentation",
          click: this.swapHandler,
          keyup: this.swapHandler,
          keydown: this.swapHandler
        }).appendTo(this.swatches)[0];
      this.altSwatchCt = this.altSwatch.getContext('2d');
      this.swatch.width = this.altSwatch.width = 70;
      this.swatch.height = this.altSwatch.height = 30;
      this.swatchLabels = $(
        "<div></div>",
        {
          "class": "colorselect-swatchlabels"
        }).appendTo(this.swatchWrap)[0];
      this.swatchLabel = $(
        "<span></span>",
        {
          "class": "colorselect-swatchlabel",
          id: getUnusedId("colorselect-swatchlabel_")
        }).text("Primary").appendTo(this.swatchLabels)[0];
      this.altSwatchLabel = $(
        "<span></span>",
        {
          "class": "colorselect-altswatchlabel",
          id: getUnusedId("colorselect-altswatchlabel_")
        }).text("Alternate").appendTo(this.swatchLabels)[0];
      this.swatchPara = $(
        "<p></p>",
        {
          "class": "colorselect-swatchpara",
          id: getUnusedId("colorselect-swatchpara_"),
          role: "note"
        }).text("Click swatch to swap colors").appendTo(this.swatchWrap)[0];
      canvasLabels =
      [
        $(this.swatchLabel).attr("id"),
        $(this.cField.field).attr("id")
      ];
      $(this.swatch).attr("aria-labelledby", canvasLabels.join(' '));
      $(".colorselect-canvascolor input", this.forms).attr(
        "aria-controls",
        [$(this.canvas).attr("id"), $(this.swatch).attr("id")].join(' '));
      canvasLabels =
      [
        $(this.altSwatchLabel).attr("id"),
        $(this.acField.field).attr("id")
      ];
      $(this.altSwatch).attr("aria-labelledby", canvasLabels.join(' '));
      this.compForms = $(
        "<form></form>",
        {
          "class": "colorselect-comp"
        }).appendTo(this.container)[0];
      this.lumField = new Field(
        {
          label: "Luminance Ratio:",
          type: "text",
          "class": "lbc",
          id: "colorselect-comp-l_"
        });
      $(this.lumField.field).attr(
        {
          "aria-readonly": true,
          "readonly": "1"
        });
      this.checkHandler = this.createCheckHandler();
      this.lumCheck = new Field(
        {
          label: "Filter colors:",
          type: "checkbox",
          "class": "lbccheck",
          eventHandler: this.checkHandler,
          value: "lum",
          checkstate: false,
          id: "colorselect-comp-c_"
        });
      this.lumRestrict = false;
      this.compRule = $("<hr />")[0];
      this.compText = $(
        "<p></p>",
        {
          "class": "colorselect-clev"
        })[0];
      this.explanation = $(
        "<p></p>",
        {
          "class": "colorselect-explain"
        })[0];
      this.linkWCAG = $(
        "<a></a>",
        {
          href: "http://www.w3.org/WAI/WCAG20/"
        }).text("WCAG 2.0").appendTo(this.explanation)[0];
      $(this.explanation).append(
        " defines Large-scale as 18 pt or 14 pt bold.");
      this.compFields = new Fieldset("compset");
      $(this.compFields.fieldset).append(
        [
          this.lumField.fieldDiv,
          this.lumCheck.fieldDiv,
          this.compRule,
          this.compText,
          this.explanation
        ]);
      $(this.compForms).append(this.compFields.fieldset);
      if (!this.compliance)
      {
        $(this.compForms).hide();
      }
      this.disabled = false;
      this.ready = true;
      this.drawSelector();
      this.keyHandler = this.createKeyHandler();
    // Mozilla repeats on keypress and Chromium repeats on keydown.  So we
    // attach to both events, and whichever gets first will remove both and
    // add them again.
      $(window).keydown(this.keyHandler).keypress(this.keyHandler);
    };

    ColorSelector.prototype.setDisabled = function (value)
    {
      if (typeof(value) !== "boolean")
      {
        return;
      }
      this.disabled = value;
      if (value)
      {
        $("input", this.container).attr("disabled", "disabled");
      }
      else
      {
        $("input", this.container).removeAttr("disabled");
      }
      this.drawSelector();
    };

    ColorSelector.prototype.createBlurHandler = function ()
    {
      var that = this;
      var blurHandler = function (event)
      {
        that.oldFocus = that.focus;
        that.focus = null;
        that.drawSelector();
      };
      return blurHandler;
    };

    ColorSelector.prototype.createFocusHandler = function ()
    {
      var that = this;
      var focusHandler = function (event)
      {
        if (that.disabled)
        {
          return;
        }
        if (that.inMouseDown)
        {
          if (this !== that.wrap)
          {
            that.oldFocus = null;
            return;
          }
          else
          {
            that.oldFocus = that.focus === "triangle" ? "ring" : "triangle";
          }
        }
        switch (this)
        {
        case that.triangleSelector:
          that.focus = "triangle";
          break;
        case that.ringSelector:
          that.focus = "ring";
          break;
        case that.wrap:
          switch (that.oldFocus)
          {
          case "ring":
            $(that.triangleSelector).focus();
            break;
          case "triangle":
            $(that.ringSelector).focus();
            break;
          default:
            return;
          }
          break;
        default:
          return;
        }
        that.drawSelector();
      };
      return focusHandler;
    };

    ColorSelector.prototype.createCheckHandler = function ()
    {
      var that = this;
      var checkHandler = function (event)
      {
        var value = this.value;
        switch (value)
        {
        case "lum":
          that.lumRestrict = !that.lumRestrict;
          break;
        case "bri":
          that.briRestrict = !that.briRestrict;
          break;
        case "col":
          that.colRestrict = !that.colRestrict;
          break;
        default:
          return;
        }
        $(this).attr("aria-checked", this.checked);
        that.drawSelector();
      };
      return checkHandler;
    };

    ColorSelector.prototype.createSwapHandler = function ()
    {
      var that = this;
      var swapHandler = function (event)
      {
        if (that.disabled || !that.allowSwap)
        {
          return;
        }
        if (event.type === "keydown" || event.type === "keyup")
        {
          if (event.keyCode !== 13) // enter key
          {
            return;
          }
          else
          {
            if (that.swapKeyed && event.type === "keydown")
            {
              return;
            }
            that.swapKeyed = !that.swapKeyed;
            if (event.type === "keyup")
            {
              return;
            }
          }
        }
        var tmp = that.color;
        that.color = that.altColor;
        that.altColor = tmp;
        $(that.container).trigger(
          'colorswap',
          [
            that.color.colorString(),
            that.altColor.colorString()
          ]);
        that.drawSelector();
      };
      return swapHandler;
    };

    ColorSelector.prototype.createKeyHandler = function ()
    {
      var that = this;
      var keyHandler = function (event)
      {
        var key = event.keyCode;
        if (key < 37 || key > 40)
        {
          return;
        } 
        else if (that.focus === "ring")
        {
          if (key < 39) // up
          {
            that.color.hueIncrement();
          }
          else // down
          {
            that.color.hueDecrement();
          }
        }
        else if (that.focus === "triangle")
        {
          var tv = that.computeTriangle();
          var s = that.color.saturation;
          var v = that.color.value;
          var x = tv.sx + (tv.vx - tv.sx) * (v / 100);
          x += (tv.hx - tv.vx) * (s / 100) * (v / 100) + 0.5;
          x = Math.floor(x);
          var y = tv.sy + (tv.vy - tv.sy) * (v / 100);
          y += (tv.hy - tv.vy) * (s / 100) * (v / 100) + 0.5;
          y = Math.floor(y);
          if (key === 37)
          {
            x -= 1;
          }
          else if (key === 38)
          {
            y -= 1;
          }
          else if (key === 39)
          {
            x += 1;
          }
          else if (key === 40)
          {
            y += 1;
          }
          var sv = that.computeSV(x, y);
          that.color.setSaturation(sv.s);
          that.color.setValue(sv.v);
        }
        $(window).unbind(
          {
            "keydown": that.keyHandler,
            "keypress": that.keyHandler
          });
        window.setTimeout(
          function ()
          {
            $(window).keydown(that.keyHandler).keypress(that.keyHandler);
          },
          30);
        $(that.container).trigger(
          "colorchange",
          [
            that.color.colorString()
          ]);
        that.drawSelector();
      };
      return keyHandler;
    };

    ColorSelector.prototype.createFieldHandler = function ()
    {
      var that = this;
      var fieldHandler = function (event)
      {
        var tmp;
        if (event.type === "keypress" && event.keyCode !== 13) // enter key
        {
          return;
        }
        try
        {
          switch (event.target)
          {
          case that.hField.field:
            tmp = that.hField.setInternalValue(event.target.value);
            if (tmp.changed)
            {
              that.color.setHue(tmp.value);
            }
            break;
          case that.sField.field:
            tmp = that.sField.setInternalValue(event.target.value);
            if (tmp.changed)
            {
              that.color.setSaturation(tmp.value);
            }
            break;
          case that.vField.field:
            tmp = that.vField.setInternalValue(event.target.value);
            if (tmp.changed)
            {
              that.color.setValue(tmp.value);
            }
            break;
          case that.rField.field:
            tmp = that.rField.setInternalValue(event.target.value);
            if (tmp.changed)
            {
              that.color.setRed(tmp.value);
            }
            break;
          case that.gField.field:
            tmp = that.gField.setInternalValue(event.target.value);
            if (tmp.changed)
            {
              that.color.setGreen(tmp.value);
            }
            break;
          case that.bField.field:
            tmp = that.bField.setInternalValue(event.target.value);
            if (tmp.changed)
            {
              that.color.setBlue(tmp.value);
            }
            break;
          case that.cField.field:
            tmp = that.cField.setInternalValue(event.target.value);
            if (tmp.changed)
            {
              that.color.setHex(tmp.value);
            }
            break;
          default:
            return;
          }
        }
        catch (ex)
        {
          return;
        }
        if (tmp.changed)
        {
          $(that.container).trigger(
            "colorchange",
            [
              that.color.colorString()
            ]);
          that.drawSelector();
        }
      };
      return fieldHandler;
    };

    ColorSelector.prototype.createDragHandler = function ()
    {
      var that = this;
      var dragHandler = function (event)
      {
        if (that.disabled)
        {
          return;
        }
        var x = event.clientX - that.canvas.offsetLeft;
        var y = event.clientY - that.canvas.offsetTop;
        var dx = x - 85;
        var dy = 85 - y;
        that.inMouseDown = true;
        if (that.inRing(dx, dy))
        {
          that.focus = "ring";
          $(that.canvas).unbind(
            "mousedown",
            that.dragHandler).css("cursor", "crosshair");
          $(window).mouseup(that.cleanHandler).mousemove(that.selectorHandler);
          that.selectorHandler(event);
        }
        else if (that.inTriangle(x, y))
        {
          that.focus = "triangle";
          $(that.canvas).unbind(
            "mousedown",
            that.dragHandler).css("cursor", "crosshair");
          $(window).mouseup(that.cleanHandler).mousemove(that.triangleHandler);
          that.triangleHandler(event);
        }
        else
        {
        }
      };
      return dragHandler;
    };

    ColorSelector.prototype.createTriangleHandler = function ()
    {
      var that = this;
      var triangleHandler = function (event)
      {
        var x = event.clientX - that.canvas.offsetLeft;
        var y = event.clientY - that.canvas.offsetTop;
        var sv;
        sv = that.computeSV(x, y);
        that.color.setSaturation(sv.s);
        that.color.setValue(sv.v);
        $(that.container).trigger(
          "colorchange",
          [
            that.color.colorString()
          ]);
        that.drawSelector();
        $(that.wrap).focus();
        event.preventDefault();
      };
      return triangleHandler;
    };

    ColorSelector.prototype.createSelectorHandler = function ()
    {
      var that = this;
      var selectorHandler = function (event)
      {
        var x = event.clientX - that.canvas.offsetLeft;
        var y = event.clientY - that.canvas.offsetTop;
        var dx = x - 85;
        var dy = 85 - y;

        var angle = Math.atan2(dy, dx);
        if (angle < 0)
        {
          angle += 2 * Math.PI;
        }
        angle *= (180 / Math.PI);
        that.color.setHue(angle);
        $(that.container).trigger(
          "colorchange",
          [
            that.color.colorString()
          ]);
        that.drawSelector();
        $(that.wrap).focus();
        event.preventDefault();
      };
      return selectorHandler;
    };

    ColorSelector.prototype.createCleanHandler = function ()
    {
      var that = this;
      var cleanHandler = function (event)
      {
        that.inMouseDown = false;
        $(window).unbind(
          {
            "mousemove": that.selectorHandler,
            "mouseup": that.cleanHandler
          }).unbind("mousemove", that.triangleHandler);
        $(that.canvas).mousedown(that.dragHandler).css("cursor", "auto");
      };
      return cleanHandler;
    };

    ColorSelector.prototype.computeTriangle = function ()
    {
      var angle = (this.color.hue / 360) * 2 * Math.PI;

      var hx = Math.floor(85 + Math.cos(angle) * 70 + 0.5);
      var hy = Math.floor(85 - Math.sin(angle) * 70 + 0.5);
      var sx = Math.floor(85 + Math.cos(angle + 2 * Math.PI / 3) * 70 + 0.5);
      var sy = Math.floor(85 - Math.sin(angle + 2 * Math.PI / 3) * 70 + 0.5);
      var vx = Math.floor(85 + Math.cos(angle + 4 * Math.PI / 3) * 70 + 0.5);
      var vy = Math.floor(85 - Math.sin(angle + 4 * Math.PI / 3) * 70 + 0.5);
      var triangleVertices =
      {
        hx: hx,
        hy: hy,
        sx: sx,
        sy: sy,
        vx: vx,
        vy: vy
      };
      return triangleVertices;
    };

    ColorSelector.prototype.computeSV = function (x, y)
    {
      var tv = this.computeTriangle();
      var s, v;
      var hx = tv.hx - 85;
      var hy = 85 - tv.hy;
      var sx = tv.sx - 85;
      var sy = 85 - tv.sy;
      var vx = tv.vx - 85;
      var vy = 85 - tv.vy;
      x -= 85;
      y = 85 - y;
      var tmpX, tmpY;
      if (vx * (x - sx) + vy * (y - sy) < 0)
      {
        s = 1;
        tmpX = hx - sx;
        tmpY = hy - sy;
        v = ((x - sx) * tmpX + (y - sy) * tmpY) / (tmpX * tmpX + tmpY * tmpY);
        v = clamp(v, 0, 1);
      }
      else if (hx * (x - sx) + hy * (y - sy) < 0)
      {
        s = 0;
        tmpX = vx - sx;
        tmpY = vy - sy;
        v = ((x - sx) * tmpX + (y - sy) * tmpY) / (tmpX * tmpX + tmpY * tmpY);
        v = clamp(v, 0, 1);
      }
      else if (sx * (x - hx) + sy * (y - hy) < 0)
      {
        v = 1;
        tmpX = hx - vx;
        tmpY = hy - vy;
        s = ((x - vx) * tmpX + (y - vy) * tmpY) / (tmpX * tmpX + tmpY * tmpY);
        s = clamp(s, 0, 1);
      }
      else
      {
        tmpX = hx - vx;
        tmpY = hy - vy;
        v = ((x - sx) * tmpY - (y - sy) * tmpX);
        v /= ((vx - sx) * tmpY - (vy - sy) * tmpX);

        if (v <= 0)
        {
          v = 0;
          s = 0;
        }
        else
        {
          if (v > 1)
          {
            v = 1;
          }

          if (Math.abs(hy - vy) < Math.abs(hx - vx))
          {
            s = (x - sx - v * (vx - sx)) / (v * (hx - vx));
          }
          else
          {
            s = (y - sy - v * (vy - sy)) / (v * (hy - vy));
          }

          s = clamp(s, 0, 1);
        }
      }
      return { s: s * 100, v: v * 100 };
    };

    ColorSelector.prototype.paintTriangle = function ()
    {
      var ct = this.context;
      var tmp, tmp2, yInterp, xStart, xEnd, xInterp, xx, yy;
      var xL, xR, xDiff;
      var h = this.color.hue;
      var s = this.color.saturation;
      var v = this.color.value;
      var altLum = this.altColor.getLuminance();
      var setL = {};
      var setR = {};
      var pad = 3;
      var p;
      var r, g, b;
    //  var rOff, gOff, bOff;
      var dis = this.disabled;
      var lum, opacity;
      var tv = this.computeTriangle();
      // This is messy, but will work for now;
      var set1 =
      {
        x: tv.hx,
        y: tv.hy
      };
      tmp = hsvToRGB({ hue: h, saturation: 100, value: 100 });
      set1.r = tmp.red;
      set1.g = tmp.green;
      set1.b = tmp.blue;
      var set2 =
      {
        x: tv.sx,
        y: tv.sy
      };
      tmp = hsvToRGB({ hue: h, saturation: 100, value: 0 });
      set2.r = tmp.red;
      set2.g = tmp.green;
      set2.b = tmp.blue;
      var set3 =
      {
        x: tv.vx,
        y: tv.vy
      };
      tmp = hsvToRGB({ hue: h, saturation: 0, value: 100 });
      set3.r = tmp.red;
      set3.g = tmp.green;
      set3.b = tmp.blue;

      if (set2.y > set3.y)
      {
        tmp = set2;
        set2 = set3;
        set3 = tmp;
      }
      if (set1.y > set3.y)
      {
        tmp = set1;
        set1 = set3;
        set3 = tmp;
      }
      if (set1.y > set2.y)
      {
        tmp = set1;
        set1 = set2;
        set2 = tmp;
      }
    //  for (yy = 20; yy < 160; yy += 1)
      for (yy = 15; yy < 155; yy += 1)
      {
        if (yy >= set1.y - pad && yy < set3.y + pad)
        {
          yInterp = clamp(yy, set1.y, set3.y);
          if (yInterp < set2.y)
          {
            tmp = set2.y - set1.y;
            if (tmp !== 0)
            {
              tmp2 = yInterp - set1.y;
              setL.x = linInterp(set1.x, set2.x, tmp2, tmp);
              setL.r = linInterp(set1.r, set2.r, tmp2, tmp);
              setL.g = linInterp(set1.g, set2.g, tmp2, tmp);
              setL.b = linInterp(set1.b, set2.b, tmp2, tmp);
            }
            else
            {
              setL.x = set1.x;
              setL.r = set1.r;
              setL.g = set1.g;
              setL.b = set1.b;
            }
          }
          else
          {
            tmp = set3.y - set2.y;
            if (tmp !== 0)
            {
              tmp2 = yInterp - set2.y;
              setL.x = linInterp(set2.x, set3.x, tmp2, tmp);
              setL.r = linInterp(set2.r, set3.r, tmp2, tmp);
              setL.g = linInterp(set2.g, set3.g, tmp2, tmp);
              setL.b = linInterp(set2.b, set3.b, tmp2, tmp);
            }
            else
            {
              setL.x = set2.x;
              setL.r = set2.r;
              setL.g = set2.g;
              setL.b = set2.b;
            }
          }
          tmp = set3.y - set1.y;
          if (tmp !== 0)
          {
            tmp2 = yInterp - set1.y;
            setR.x = linInterp(set1.x, set3.x, tmp2, tmp);
            setR.r = linInterp(set1.r, set3.r, tmp2, tmp);
            setR.g = linInterp(set1.g, set3.g, tmp2, tmp);
            setR.b = linInterp(set1.b, set3.b, tmp2, tmp);
          }
          else
          {
            setR.x = set1.x;
            setR.r = set1.r;
            setR.g = set1.g;
            setR.b = set1.b;
          }
          if (setL.x > setR.x)
          {
            tmp = setL;
            setL = setR;
            setR = tmp;
          }
          p = yy * 680;
          xStart = Math.max(setL.x - pad, 15);
          xEnd = Math.min(setR.x + pad, 155);
          xStart = Math.min(xStart, xEnd);
          r = setL.r;
          g = setL.g;
          b = setL.b;
          opacity = 255;
          if (dis)
          {
            opacity = clamp(opacity - 100, 0, 255);
          }
          for (xx = Math.floor(xStart - 10); xx < xStart; xx += 1)
          {
            tmp = p + xx * 4;
            this.img.data[tmp] = r;
            tmp += 1;
            this.img.data[tmp] = g;
            tmp += 1;
            this.img.data[tmp] = b;
            tmp += 1;
            this.img.data[tmp] = opacity;
          }
          xL = setL.x;
          xR = setR.x;
          xDiff = xR - xL;
          for (; xx < xEnd; xx += 1)
          {
            opacity = 255;
            if (dis)
            {
              opacity = clamp(opacity - 100, 0, 255);
            }
            xInterp = clamp(xx, xL, xR);
            if (xDiff !== 0)
            {
              tmp2 = xInterp - xL;
              r = linInterp(setL.r, setR.r, tmp2, xDiff);
              g = linInterp(setL.g, setR.g, tmp2, xDiff);
              b = linInterp(setL.b, setR.b, tmp2, xDiff);
            }
            else
            {
              r = setL.r;
              g = setL.g;
              b = setL.b;
            }
            if (this.lumRestrict)
            {
              //tmp = invert({ red: r, green: g, blue: b });
              //r = tmp.red;
              //g = tmp.green;
              //b = tmp.blue;
              if (xx % 2 === 0) // Speedup for older browsers/processors.
              {
                lum = getLumRatio(
                  altLum,
                  getLuminanceColor(r) * 0.2126 +
                  getLuminanceColor(g) * 0.7152 +
                  getLuminanceColor(b) * 0.0722);
              }
              if (lum < 3)
              {
                opacity = 105;
              }
              else if (lum < 4.5)
              {
                opacity = 155;
              }
              else if (lum < 7)
              {
                opacity = 205;
              }
            }
            tmp = p + xx * 4;
            this.img.data[tmp] = r;
            tmp += 1;
            this.img.data[tmp] = g;
            tmp += 1;
            this.img.data[tmp] = b;
            tmp += 1;
            this.img.data[tmp] = opacity;
          }
          r = setR.r;
          g = setR.g;
          b = setR.b;
          xEnd += 10;
          opacity = 255;
          if (dis)
          {
            opacity = clamp(opacity - 100, 0, 255);
          }
          for (; xx < xEnd; xx += 1)
          {
            tmp = p + xx * 4;
            this.img.data[tmp] = r;
            tmp += 1;
            this.img.data[tmp] = g;
            tmp += 1;
            this.img.data[tmp] = b;
            tmp += 1;
            this.img.data[tmp] = opacity;
          }
        }
      }
      this.fakeCt.putImageData(this.img, 0, 0);
      ct.fillStyle = ct.createPattern(this.fakeCanvas, "no-repeat");
      ct.beginPath();
      ct.moveTo(set1.x, set1.y);
      ct.lineTo(set2.x, set2.y);
      ct.lineTo(set3.x, set3.y);
      ct.closePath();
      ct.fill();
      v /= 100;
      s /= 100;
      xx = tv.sx + (tv.vx - tv.sx) * v;
      xx += (tv.hx - tv.vx) * s * v + 0.5;
      xx = Math.floor(xx);
      yy = tv.sy + (tv.vy - tv.sy) * v;
      yy += (tv.hy - tv.vy) * s * v + 0.5;
      yy = Math.floor(yy);

      if (this.color.intensity() > 0.5)
      {
        ct.strokeStyle = "rgb(64, 64, 64)";
      }
      else
      {
        ct.strokeStyle = "rgb(192, 192, 192)";
      }
      ct.lineWidth = 2;
      ct.beginPath();
      ct.arc(xx, yy, 4, 0, 2 * Math.PI, true);
      ct.closePath();
      ct.stroke();
      if (this.focus === "triangle")
      {
        ct.lineWidth = 1;
        if (ct.strokeStyle === "#404040")
        {
          ct.strokeStyle = "rgba(96, 96, 96, 0.75)";
        }
        else
        {
          ct.strokeStyle = "rgba(160, 160, 160, 0.75)";
        }
        ct.strokeRect(
          xx - pad - 4 + 0.5,
          yy - pad - 4 + 0.5,
          2 * (4 + pad) - 1,
          2 * (4 + pad) - 1);
      }
    };

    ColorSelector.prototype.setForms = function ()
    {
      if (this.useHSVFields)
      {
        this.hField.setValue(parseFloat(this.color.hue) + 0.5);
        this.sField.setValue(parseFloat(this.color.saturation) + 0.5);
        this.vField.setValue(parseFloat(this.color.value) + 0.5);
      }
      if (this.useRGBFields)
      {
        this.rField.setValue(parseFloat(this.color.red) + 0.5);
        this.gField.setValue(parseFloat(this.color.green) + 0.5);
        this.bField.setValue(parseFloat(this.color.blue) + 0.5);
      }
      if (this.useColorField)
      {
        this.cField.setValue(this.color.colorString());
        if (this.useAltColor)
        {
          this.acField.setValue(this.altColor.colorString());
        }
      }
      if (this.compliance)
      {
        var lDiff = getColorLumRatio(this.color, this.altColor);
        var lPass = lDiff >= 3.0 ? lDiff >= 4.5 ? lDiff >= 7.0 ? 3 : 2 : 1 : 0;
        var compLevel;
        switch (lPass)
        {
        case 3:
          compLevel = "AAA";
          break;
        case 2:
          compLevel = "AA; AAA if Large-scale";
          break;
        case 1:
          compLevel = "AA if Large-scale";
          break;
        default:
          compLevel = "Noncompliant";
          break;
        }
        lDiff = lDiff.toFixed(3).split(''); // So we can truncate/not round
        lDiff.pop();
        this.lumField.setValue(lDiff.join(''));
        $(this.compText).text("WCAG 2.0 Status: " + compLevel);
      }
    };

    ColorSelector.prototype.setSwatch = function ()
    {
      var ct = this.swatchCt;
      var c, ac;
      if (this.disabled)
      {
        c = this.color.desaturated();
        ac = this.altColor.desaturated();
      }
      else
      {
        c = this.color.rgbString();
        ac = this.altColor.rgbString();
      }
      ct.fillStyle = c;
      var sampleText = this.sampleText.split('');
      var sampleFont =
      [
        this.sampleTextSize,
        [
          '"',
          this.sampleTextFont,
          '",'
        ].join(''),
        this.sampleTextFamily
      ].join(' ');
      ct.font = sampleFont;
      var textWidth = 70 - ct.measureText(sampleText.join('')).width;
      while (textWidth < 0)
      {
        sampleText.pop();
        textWidth = 70 - ct.measureText(sampleText.join('')).width;
      }
      sampleText = sampleText.join('');
      textWidth /= 2;
      var textHeight = 15 + (ct.measureText("m").width * 0.9) / 2;
      textHeight = clamp(textHeight, 15, 30);
      ct.fillRect(0, 0, ct.canvas.width, ct.canvas.height);
      ct.fillStyle = ac;
      ct.fillText(sampleText, textWidth, textHeight);
      if (this.useAltColor)
      {
        ct = this.altSwatchCt;
        ct.fillStyle = ac;
        ct.font = sampleFont;
        ct.fillRect(0, 0, ct.canvas.width, ct.canvas.height);
        ct.fillStyle = c;
        ct.fillText(sampleText, textWidth, textHeight);
      }
      if (this.allowSwap && this.useAltColor)
      {
        $(this.swatchPara).show();
      }
      else
      {
        $(this.swatchPara).hide();
      }
    };

    ColorSelector.prototype.drawSelector = function ()
    {
      var ct = this.context;
      this.canvas.width = this.canvas.width;
      var colorHue = this.color.hue;
      var scos, ssin;
      var tcol;
      ct.lineWidth = 4;
      ct.translate(85, 85);
      scos = Math.cos(Math.PI / 90);
      ssin = Math.sin(Math.PI / 90);
      var satur = 100;
      if (this.disabled)
      {
        satur -= 50;
      }
      for (var i = 0; i < 360; i += 2)
      {
        tcol = hsvToRGB(
          {
            hue: i,
            saturation: satur,
            value: 100
          });
        ct.strokeStyle = colorString(tcol.red, tcol.green, tcol.blue);
        ct.beginPath();
        ct.moveTo(70, 0);
        ct.lineTo(85, 0);
        ct.stroke();
        ct.closePath();
        ct.transform(scos, -ssin, ssin, scos, 0, 0);
      }
      
      ct.lineWidth = 2;
      if (colorHue < 221 && colorHue > 20)
      {
        ct.strokeStyle = "#000";
      }
      else
      {
        ct.strokeStyle = "#FFF";
      }

      scos = Math.cos(Math.PI * 2 * (colorHue / 360));
      ssin = Math.sin(Math.PI * 2 * (colorHue / 360));

      try
      {
        ct.transform(scos, -ssin, ssin, scos, 0, 0);
      }
      catch (ex)
      {
        alert(ex);
      }

      ct.beginPath();
      ct.moveTo(70, 0);
      ct.lineTo(85, 0);
      ct.stroke();
      ct.closePath();

      scos = Math.cos(Math.PI * 2 * ((360 - colorHue) / 360));
      ssin = Math.sin(Math.PI * 2 * ((360 - colorHue) / 360));
      ct.transform(scos, -ssin, ssin, scos, 0, 0);
      ct.translate(-85, -85);
      this.paintTriangle();
      this.setForms();
      this.setSwatch();
      if (this.focus === "ring")
      {
        this.canvas.style.border = "1px dotted rgba(0, 0, 0, 1)";
      }
      else
      {
        this.canvas.style.border = "1px solid rgba(0, 0, 0, 0)";
      }
    };

    ColorSelector.prototype.inRing = function (x, y)
    {
      var dx = x;
      var dy = y;
      var dist = dx * dx + dy * dy;
      return (dist >= 70 * 70 && dist <= 85 * 85);
    };

    ColorSelector.prototype.inTriangle = function (x, y)
    {
      var tv = this.computeTriangle(this.color.hue);
      var det = (tv.vx - tv.sx) * (tv.hy - tv.sy);
      det -= (tv.vy - tv.sy) * (tv.hx - tv.sx);
      var s = ((x - tv.sx) * (tv.hy - tv.sy) -
        (y - tv.sy) * (tv.hx - tv.sx)) / det;
      var v = ((tv.vx - tv.sx) * (y - tv.sy) -
        (tv.vy - tv.sy) * (x - tv.sx)) / det;
      s *= 100;
      v *= 100;
      return (s >= 0 && v >= 0 && s + v <= 100);
    };

/* END PRIVATE FUNCTIONS */

    var methods =
    {
      init: function (options)
      {
        var opts = $.extend({}, $.fn.colorselect.defaults, options);
        return this.each(function ()
        {
          var $this = $(this),
              data = $this.data('colorselect'),
              colorselect = new ColorSelector(
                this,
                opts);
          if (!data)
          {
            $(this).data(
              'colorselect',
              {
                target: $this,
                colorselect: colorselect
              });
            $this.attr("class", "colorselect");
            colorselect.initialize();
          }
          if (typeof(opts.colorchange) === "function")
          {
            $this.bind("colorchange", opts.colorchange);
          }
          if (typeof(opts.altcolorchange) === "function")
          {
            $this.bind("altcolorchange", opts.altcolorchange);
          }
          if (typeof(opts.colorswap) === "function")
          {
            $this.bind("colorswap", opts.colorswap);
          }
        });
      },
      disable: function ()
      {
        return this.each(function ()
        {
          var $this = $(this),
              data = $this.data('colorselect');
          data.colorselect.setDisabled(true);
        });
      },
      enable: function ()
      {
        return this.each(function ()
        {
          var $this = $(this),
              data = $this.data('colorselect');
          data.colorselect.setDisabled(false);
        });
      },
      isDisabled: function ()
      {
        return $(this).data('colorselect').colorselect.disabled;
      },
      getColor: function ()
      {
        return $(this).data('colorselect').colorselect.color.colorString();
      },
      getAltColor: function ()
      {
        return $(this).data('colorselect').colorselect.altColor.colorString();
      },
      option: function (option, value)
      {
        // Just return the first value if it's a set.
        // Copying behavior of jQueryUI here, normally I'd return an array.
        if (typeof(option) === "string" && typeof(value) === "undefined")
        {
          var $this = $($(this)[0]),
              data = $this.data('colorselect');
          switch (option)
          {
          case "color":
            return data.colorselect.color.colorString();
          case "altColor":
            return data.colorselect.altColor.colorString();
          case "compliance":
            return data.colorselect.compliance;
          case "allowSwap":
            return data.colorselect.allowswap;
          case "useHSVFields":
            return data.colorselect.useHSVFields;
          case "useRGBFields":
            return data.colorselect.useRGBFields;
          case "useColorField":
            return data.colorselect.useColorField;
          case "useColorSwatch":
            return data.colorselect.useColorSwatch;
          case "useAltColor":
            return data.colorselect.useAltColor;
          case "sampleText":
            return data.colorselect.sampleText;
          case "sampleTextSize":
            return data.colorselect.sampleTextSize;
          case "sampleTextFont":
            return data.colorselect.sampleTextFont;
          case "sampleTextFamily":
            return data.colorselect.sampleTextFamily;
          default:
            return;
          }
        }
        return this.each(function ()
        {
          var $this = $(this),
              data = $this.data('colorselect');
          if (typeof(option) === "object")
          {
            for (var key in option)
            {
              if (option.hasOwnProperty(key))
              {
                switch (key)
                {
                case "colorchange":
                  $this.bind(
                    "colorchange",
                    option[key]);
                  break;
                case "altcolorchange":
                  $this.bind(
                    "altcolorchange",
                    option[key]);
                  break;
                case "colorswap":
                  $this.bind(
                    "colorswap",
                    option[key]);
                  break;
                case "color":
                  data.colorselect.setColor(option[key]);
                  break;
                case "altColor":
                  data.colorselect.setAltColor(option[key]);
                  break;
                case "compliance":
                  data.colorselect.setCompliance(option[key]);
                  break;
                case "allowSwap":
                  data.colorselect.setAllowSwap(option[key]);
                  break;
                case "useHSVFields":
                  data.colorselect.setUseHSVFields(option[key]);
                  break;
                case "useRGBFields":
                  data.colorselect.setUseRGBFields(option[key]);
                  break;
                case "useColorField":
                  data.colorselect.setUseColorField(option[key]);
                  break;
                case "useColorSwatch":
                  data.colorselect.setUseColorSwatch(option[key]);
                  break;
                case "useAltColor":
                  data.colorselect.setUseAltColor(option[key]);
                  break;
                case "sampleText":
                  data.colorselect.setSampleText(option[key]);
                  break;
                case "sampleTextSize":
                  data.colorselect.setSampleTextSize(option[key]);
                  break;
                case "sampleTextFont":
                  data.colorselect.setSampleTextFont(option[key]);
                  break;
                case "sampleTextFamily":
                  data.colorselect.setSampleTextFamily(option[key]);
                  break;
                default:
                }
              }
            }
          }
          else if (typeof(option) === "string")
          {
            switch (option)
            {
            case "colorchange":
              $this.bind(
                "colorchange",
                value);
              break;
            case "altcolorchange":
              $this.bind(
                "altcolorchange",
                value);
              break;
            case "colorswap":
              $this.bind(
                "colorswap",
                value);
              break;
            case "color":
              data.colorselect.setColor(value);
              break;
            case "altColor":
              data.colorselect.setAltColor(value);
              break;
            case "compliance":
              data.colorselect.setCompliance(value);
              break;
            case "allowSwap":
              data.colorselect.setAllowSwap(value);
              break;
            case "useHSVFields":
              data.colorselect.setUseHSVFields(value);
              break;
            case "useRGBFields":
              data.colorselect.setUseRGBFields(value);
              break;
            case "useColorField":
              data.colorselect.setUseColorField(value);
              break;
            case "useColorSwatch":
              data.colorselect.setUseColorSwatch(value);
              break;
            case "useAltColor":
              data.colorselect.setUseAltColor(value);
              break;
            case "sampleText":
              data.colorselect.setSampleText(value);
              break;
            case "sampleTextSize":
              data.colorselect.setSampleTextSize(value);
              break;
            case "sampleTextFont":
              data.colorselect.setSampleTextFont(value);
              break;
            case "sampleTextFamily":
              data.colorselect.setSampleTextFamily(value);
              break;
            default:
            }
          }
        });
      },
      show: function ()
      {
        return this.each(function ()
        {
          var $this = $(this),
              data = $this.data('colorselect');
          $(data.colorselect.container).hide();
        });
      },
      hide: function ()
      {
        return this.each(function ()
        {
          var $this = $(this),
              data = $this.data('colorselect');
          $(data.colorselect.container).show();
        });
      },
      destroy: function ()
      {
        return this.each(function ()
        {
          var $this = $(this),
              data = $this.data('colorselect');
          $(window).unbind('.colorselect');
          data.colorselect.remove();
          $this.removeData('colorselect');
        });
      }
    };
    $.fn.colorselect = function (method)
    {
      if (methods[method])
      {
        return methods[method].apply(
          this,
          Array.prototype.slice.call(arguments, 1));
      }
      else if (typeof(method) === "object" || !method)
      {
        return methods.init.apply(this, arguments);
      }
      else
      {
        $.error("Method " + method + " does not exist for jQuery.colorselect");
      }
    };
    $.fn.colorselect.defaults =
    {
      color: "#7B0000",
      altColor: "#CCC",
      useAltColor: true,
      compliance: false,
      allowSwap: true,
      useRGBFields: true,
      useHSVFields: true,
      useColorField: true,
      useColorSwatch: true,
      sampleText: "Hello",
      sampleTextSize: "18pt",
      sampleTextFont: "Arial",
      sampleTextFamily: "sans-serif"
    };
  }(jQuery));
